Skip to content

fix(sqlite): backfill _sqlite_rowid slot on pre-existing rel tables - #143

Merged
adsharma merged 2 commits into
LadybugDB:mainfrom
adsharma:sqlite-rid-slot
Nov 9, 2026
Merged

adsharma merged 2 commits into
LadybugDB:mainfrom
adsharma:sqlite-rid-slot

Conversation

@adsharma

@adsharma adsharma commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #142 — please merge #142 first; once it lands this diff shrinks to the single slot commit below. (Opened against main because the #142 branch only exists on the fork.)

Problem. Edge upserts address rows by the _sqlite_rowid slot (MATCH ()-[e:Label {_sqlite_rowid}] DELETE + CREATE … {_sqlite_rowid}). Tables the sync itself creates carry the slot from buildRelDdl, but explicitly mapped pre-existing native tables (e.g. MentionedIn/RelatesTo/ConflictsWith) don't — so the first edge statement of any tick failed with Binder exception: Cannot find property _sqlite_rowid, and (pre-#142) the already-committed node rows were left behind as partial state.

Change (sqlite/src/function/sync_engine.cpp, DDL phase): for every mapped edge table, ALTER TABLE <label> ADD IF NOT EXISTS _sqlite_rowid INT64. Idempotent and replay-safe; runs with the other DDL outside the per-batch transactions. Pre-sync edges read NULL for the slot — correct, they are not sync-owned, and endpoint-based deletes still cover them.

Validation (consumer-side, ladybug-memory-rs sqlite-first path through the real engine + this build):

  • Staged memory + entity + mention + co-occurrence replicate end-to-end with via=extension, no fallback: 1 memory, 2 entities, 2 mentions, cursor advanced, second tick reports caught up.
  • A second remember on the same intake DB replicates incrementally (WAL-restart reconcile path), reusing the existing entity node instead of forking it; native counts stay exact, no twins.
  • Why the slot must exist rather than be skipped: SQLite reuses rowids after deletes (max+1), so endpoint-keyed matching cannot replace a moved edge — the slot gives exact row identity for the slot-clear DELETE.

One SQLite WAL commit now maps to one Ladybug transaction: the batch's
statements plus its cursor advance commit atomically. A mid-tick failure
(e.g. a binder error on one edge table) leaves no partial rows behind,
and the next tick replays whole batches idempotently.

- applyBatchTx wraps BEGIN .. batch .. cursor .. COMMIT with ROLLBACK on
  any statement failure; stats only count committed work.
- Cursor advances per batch instead of once at tick end, so crash recovery
  resumes from the last committed SQLite commit.
- DDL stays outside the transaction (replay-safe: IF NOT EXISTS /
  one-shot shadow healing).
Edge upserts address rows by the _sqlite_rowid slot (delete-by-slot +
CREATE), and tables the sync creates carry it from buildRelDdl — but
explicitly mapped pre-existing native tables don't, so the whole tick
failed with 'Binder exception: Cannot find property _sqlite_rowid'.

Issue ALTER TABLE <label> ADD IF NOT EXISTS _sqlite_rowid INT64 for
every mapped edge table in the DDL phase (idempotent, replay-safe, and
outside the per-batch transactions). Pre-sync edges read NULL — correct,
they are not sync-owned; endpoint deletes still cover them.
@adsharma
adsharma merged commit 2d7420e into LadybugDB:main Nov 9, 2026
2 checks passed
@adsharma
adsharma deleted the sqlite-rid-slot branch November 9, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant